package org.haptimap.hcimodules.tactilecompass;

import org.haptimap.hcimodules.HCIModule;

import android.content.Context;
import android.location.Location;
import android.os.Vibrator;
import android.util.Log;

public class TactileCompass extends HCIModule implements Runnable {

	// ========================================================================
	// Constant Fields
	// ========================================================================

	public static final int			DIRECTION_ENC_TWO_PULSE		= 1;
	public static final int			DIRECTION_ENC_PULSE_COUNT	= 2;

	protected static final String	TAG							= "TactileCompass";

	// Vibration constants for distance encoding
	private static final int		MIN_SLEEP_TIME				= 1500;
	private static final int		MAX_SLEEP_TIME				= 5000;
	private static final int		DEF_SLEEP_TIME				= 3000;
	private static final int		VAR_SLEEP_TIME				= MAX_SLEEP_TIME
																		- MIN_SLEEP_TIME;
	private static final int		MAX_SEC						= 180;

	private static final int		NORMAL_PULSE				= 100;
	private static final int		PAUSE						= 250;
	private static final int		DO_NOT_REPEAT				= -1;

	// ========================================================================
	// Fields
	// ========================================================================

	// Customization
	private boolean					muted;
	private boolean					announceDestinationReached;
	private boolean					playSinglePattern;
	private boolean					encodeDistance;
	private boolean					silentCorridorOn;

	// System sensors and actuators
	private Vibrator				vibrator;

	// BearingModule
	private BearingModule			bearingModule;
	private HeadingModule			headingModule;

	// Thread
	private Thread					runner;
	private boolean					finished					= false;
	private long					lastInterruption;

	private int						directionEncodingMode		= DIRECTION_ENC_TWO_PULSE;
	private int						numberOfDirections			= 4;

	// ========================================================================
	// Constructors
	// ========================================================================

	/**
	 * Creates a new TactileCompass
	 * @param context Application context
	 */
	public TactileCompass(Context context) {
		super(context);

		// Init sensors and actuators
		this.vibrator = (Vibrator) context
				.getSystemService(Context.VIBRATOR_SERVICE);

		this.bearingModule = new BearingModule(context);
		this.headingModule = bearingModule.getHeadingModule();
	}

	// ========================================================================
	// Methods
	// ========================================================================

	public void triggerSinglePattern() {
		Log.i(TAG, "trigger single pattern");
		this.playSinglePattern = true;
		this.interrupt();
	}

	// ------------------------------------------------------------------------
	// Getters & Setters
	// ------------------------------------------------------------------------

	public void setDestination(Location destination) {
		this.bearingModule.setDestination(destination);
	}
	
	public BearingModule getBearingModule() {
		return this.bearingModule;
	}

	public void setMuted(boolean muted) {
		this.muted = muted;
	}

	public boolean isMuted() {
		return muted;
	}

	public boolean isEncodeDistance() {
		return encodeDistance;
	}

	public void setEncodeDistance(boolean encodeDistance) {
		this.encodeDistance = encodeDistance;
	}

	public void announceDestinationReached() {
		this.announceDestinationReached = true;
	}

	public void setSilentCorridor(boolean silentCorridorOn) {
		this.silentCorridorOn = silentCorridorOn;
	}

	public void setNumberOfDirections(int dir) {
		this.numberOfDirections = dir;
	}

	public int getNumberOfDirections() {
		return numberOfDirections;
	}

	public void setDirectionEncodingMode(int mode) {
		this.directionEncodingMode = mode;
	}

	public void setBearingModule(BearingModule bearingModule) {
		this.bearingModule = bearingModule;
	}

	// ------------------------------------------------------------------------
	// Implementation of 'HCIModule'
	// ------------------------------------------------------------------------

	/** Starts the tactile compass' internal thread. */
	@Override
	public void onStart() {
		Log.i(TAG, "onStart()");
		if (runner == null) {
			this.finished = false;
			this.runner = new Thread(this);
			this.runner.setName(this.getClass().getSimpleName());
			this.runner.setPriority(Thread.NORM_PRIORITY + 1);
			this.runner.setDaemon(true);
			this.runner.start();
		}

		this.headingModule.onStart();
	}

	/** Stops the tactile compass' internal thread. */
	@Override
	public void onStop() {
		Log.i(TAG, "onStop()");

		this.headingModule.onStop();

		this.finished = true;
		try {
			this.interrupt();
		} catch (Exception e) {
			Log.w(TAG, e + " in onStop()");
		}
		Log.i(TAG, "runner interrupted");
		this.runner = null;
		this.vibrator.cancel();

	}

	@Override
	public void onPause() {
	}

	@Override
	public void onResume() {
	}

	@Override
	public void onDestroy() {
	}

	// ------------------------------------------------------------------------
	// Implementation of 'Runnable'
	// ------------------------------------------------------------------------

	/**
	 * Called by the internal thread to control the vibration output.
	 */
	public void run() {

		int sleep = MAX_SLEEP_TIME;

		while (!finished) {
			try {

				sleep = computeSleepTime();

				// Inform user if destination has been reached
				if (announceDestinationReached) {
					this.encodeDestinationReached();
				}

				// If bearing module has destination
				else if (bearingModule != null
						&& bearingModule.hasDestination()) {

					// otherwise if bearing module has valid data
					if (bearingModule.hasValidData()) {
						int direction = this.bearingModule
								.getRelativeDirection();
						this.encodeDirection(direction);

						// user is walking but GPS is too bad
					} else if (!bearingModule.getHeadingModule().isStanding()) {
						this.encodeInsufficientGpsAccuracy();
						sleep = MAX_SLEEP_TIME * 2;
					}
					// else = user has sufficient GPS accuracy but is standing
				}

				if (!playSinglePattern)
					Thread.sleep(sleep);

			} catch (InterruptedException ie) {

			} catch (Exception e) {
				Log.e(TAG, e + " in run() ", e);
			}
		}

		Log.i(TAG, "run() method left");
	}

	public void interrupt() {
		this.vibrator.cancel();
		if (runner != null)
			this.runner.interrupt();
	}

	// ------------------------------------------------------------------------
	// Implementation of 'GeoCompassListener'
	// ------------------------------------------------------------------------

	public void onStartScanningMode() {

		long currentTime = System.currentTimeMillis();
		if ((currentTime - lastInterruption) > MIN_SLEEP_TIME) {
			this.interrupt();
		}
		this.lastInterruption = currentTime;
	}

	// ------------------------------------------------------------------------
	// Vibration handling
	// ------------------------------------------------------------------------

	/**
	 * Encode the direction of the destination if it is further away than 50m.
	 * @param relDirection the direction of the destination in relation to the
	 *            user's orientation
	 * @throws InterruptedException if the thread that controls the vibration is
	 *             interrupted
	 */
	public int encodeDirection(int direction) throws InterruptedException {

		final int signedDirection = signedDirection(direction);
		final int unsignedDirection = Math.abs(signedDirection);

		boolean right = signedDirection >= 0;

		int length = determinePulseLength(unsignedDirection);

		// Log.i(TAG, "play single pattern == "+playSinglePattern);

		// if silent mode is on AND waypoint is ahead AND no single pattern is
		// requested AND we in Walking Mode ... do NOT vibrate
		if (silentCorridorOn
				&& length == NORMAL_PULSE
				&& !playSinglePattern
				&& !bearingModule.getHeadingModule().isUsingPointingDirection()) {
			// Log.d(TAG, "silent mode -> not playing 'ahead' pattern");
			return 2 * NORMAL_PULSE + PAUSE;
		}

		int halfFrontCorridor = getFrontCorridorSize() / 2;

		if (directionEncodingMode == DIRECTION_ENC_PULSE_COUNT)
			return encodeDirectionPulseCount(
					unsignedDirection,
					right,
					length,
					halfFrontCorridor);

		// DIRECTION_ENC_TWO_PULSE
		return encodeDirectionTwoPulse(
				unsignedDirection,
				right,
				length,
				halfFrontCorridor);
	}

	protected int encodeDirectionTwoPulse(final int unsignedDirection,
			boolean right,
			int length,
			int frontCorridor) throws InterruptedException {

		// behind
		if (unsignedDirection > (180 - frontCorridor)) {
			long[] pattern = {
					0,
					NORMAL_PULSE,
					PAUSE,
					NORMAL_PULSE,
					PAUSE,
					NORMAL_PULSE };
			return vibrate(pattern);

			// right & front
		} else if (right) {
			long[] pattern = { 0, NORMAL_PULSE, PAUSE, length };
			return vibrate(pattern);

			// left
		} else {
			long[] pattern = { 0, length, PAUSE, NORMAL_PULSE };
			return vibrate(pattern);
		}
	}

	protected int encodeDirectionPulseCount(final int unsignedDirection,
			boolean right,
			int length,
			int frontCorridor) throws InterruptedException {

		// Behind -> 4 pulses
		if (unsignedDirection > (180 - frontCorridor)) {
			long[] pattern = {
					0,
					NORMAL_PULSE,
					PAUSE,
					NORMAL_PULSE,
					PAUSE,
					NORMAL_PULSE,
					PAUSE,
					NORMAL_PULSE };
			return vibrate(pattern);

			// ahead -> 1 pulse
		} else if (unsignedDirection < frontCorridor) {
			long[] pattern = { 0, NORMAL_PULSE };
			return vibrate(pattern);

			// left -> 2 pulses
		} else if (!right) {
			long[] pattern = { 0, NORMAL_PULSE, PAUSE, NORMAL_PULSE };
			return vibrate(pattern);

			// right = -> 3 pulses
		} else {
			long[] pattern = {
					0,
					NORMAL_PULSE,
					PAUSE,
					NORMAL_PULSE,
					PAUSE,
					NORMAL_PULSE };
			return vibrate(pattern);
		}
	}

	protected int encodeDestinationReached() throws InterruptedException {
		Log.i(TAG, "encodeDestinationReached()");
		this.announceDestinationReached = false;

		final long[] pattern = { 0, // initial pause
				NORMAL_PULSE, // pulse
				NORMAL_PULSE, // pause
				NORMAL_PULSE, // pulse
				NORMAL_PULSE, // pause
				2 * NORMAL_PULSE, // pulse
				NORMAL_PULSE, // pause

				NORMAL_PULSE, // pulse
				NORMAL_PULSE, // pause
				NORMAL_PULSE, // pulse
				NORMAL_PULSE, // pause
				2 * NORMAL_PULSE, // pulse
				NORMAL_PULSE, // pause

				NORMAL_PULSE, // pulse
				NORMAL_PULSE, // pause
				NORMAL_PULSE, // pulse
				NORMAL_PULSE, // pause
				2 * NORMAL_PULSE, // pulse
				NORMAL_PULSE, // pause

				NORMAL_PULSE, // pulse
				NORMAL_PULSE, // pause
				NORMAL_PULSE, // pulse
				NORMAL_PULSE, // pause
				2 * NORMAL_PULSE, // pulse
				NORMAL_PULSE, // pause
		};
		return vibrate(pattern);
	}

	/**
	 * Encodes that the GPS signal is lost
	 * @throws InterruptedException if the thread that controls the vibration is
	 *             interrupted
	 */
	protected void encodeInsufficientGpsAccuracy() throws InterruptedException {
		// long[] pattern = { 0, NORMAL_PULSE / 2 };
		// vibrate(pattern);
		// error pattern has shown to be confusing => turned off
	}

	/**
	 * A blocking call of the tactor which additionally encoded intensity in a
	 * PWM signal.
	 * @param duration the duration of the signal in ms
	 * @param intensity the intensity of the signal, values from 0 to 100 are
	 *            accepted
	 * @throws InterruptedException if waiting for the vibration signal to end
	 *             is being interrupted
	 */
	private int vibrate(long[] pattern) throws InterruptedException {

		// play if single pattern requested or scanning mode is enabled and used
		if (playSinglePattern || !muted) {
			this.vibrator.vibrate(pattern, DO_NOT_REPEAT);
			this.playSinglePattern = false;
		}
		return 0;
	}

	// ------------------------------------------------------------------------
	// Dealing with distances and 4, 6, 8 directions to display
	// ------------------------------------------------------------------------

	private int computeSleepTime() {

		int sleepTime = MAX_SLEEP_TIME;
		double timeUntilDest = this.bearingModule.timeUntilDestination();

		if (encodeDistance) {

			// If time until destination is valid but short than maximum seconds
			if (timeUntilDest > 0 && timeUntilDest < MAX_SEC)

				// Calculate variable sleep time
				sleepTime = (int) (VAR_SLEEP_TIME * timeUntilDest / MAX_SEC + MIN_SLEEP_TIME);

		} else {
			// No distance encoding -> average sleep time
			sleepTime = DEF_SLEEP_TIME;
		}

		// Log.i(TAG, "time until dest "
		// + Util.format(timeUntilDest, 100)
		// + "s => "
		// + sleepTime
		// + "s sleep time");

		return sleepTime;
	}

	private int determinePulseLength(int unsignedDirection) {

		int halfFrontCorridor = getFrontCorridorSize() / 2;
		int length = NORMAL_PULSE;

		switch (numberOfDirections) {

		// Eight directions
		case 8:
			if (unsignedDirection > halfFrontCorridor)
				length = (int) (2.7 * NORMAL_PULSE);

			if (unsignedDirection > 67)
				length = (int) (4.6 * NORMAL_PULSE);

			if (unsignedDirection > 112)
				length = (int) (8 * NORMAL_PULSE);

			break;

		// Six directions
		case 6:

			if (unsignedDirection > halfFrontCorridor)
				length = (int) (3.6 * NORMAL_PULSE);

			if (unsignedDirection > 90)
				length = (int) (7.7 * NORMAL_PULSE);

			break;

		// Four directions
		default:
			if (unsignedDirection > halfFrontCorridor)
				length = (int) (4 * NORMAL_PULSE);
			break;
		}

		return length;
	}

	private int getFrontCorridorSize() {

		switch (numberOfDirections) {
		case 8:
			return 45;
		case 6:
			return 60;
		default:
			return 90;
		}
	}

	// ------------------------------------------------------------------------
	// Helpers
	// ------------------------------------------------------------------------

	private static int signedDirection(int relDirection) {
		final int signedDirection = (relDirection > 180
				? relDirection - 360
				: relDirection);
		return signedDirection;
	}

}
